package previewer

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/gorilla/websocket"
)

func NewHarmonicaPreviewer(previewerId, host string, port int, wsTimeout int) (*HarmonicaPreviewer, error) {
	ret := &HarmonicaPreviewer{
		// config:             config,
		previewerId: previewerId,
		// sessionId:   sessionId,
		host: host,
		port: port,
		// jsBundleLoaderPort: jsBundleLoaderPort,
		wsTimeout: wsTimeout,
	}

	// backendUrl := fmt.Sprintf("ws://%v", GetBackendHostname(ret.instanceId))
	// dialer := websocket.DefaultDialer
	// requestHeader := http.Header{}
	// log.Printf("[previewer-proxy] dial backend %v", backendUrl)
	// connBackend, _, err := dialer.Dial(backendUrl, requestHeader)
	// if err != nil {
	// 	log.Printf("websocketproxy: dial backend failed, %v", err)
	// 	return nil, err
	// }
	// ret.connBackend = connBackend
	return ret, nil
}

type HarmonicaPreviewer struct {
	// config             PreviewerConfig
	previewerId string
	// sessionId   string

	host string
	port int

	wsTimeout int // seconds
	// jsBundleLoaderPort int

	// connBackend *websocket.Conn
}

func (p *HarmonicaPreviewer) getBackendHostname() string {
	return fmt.Sprintf("%v:%v", p.host, p.port)
}

// func (p *HarmonicaPreviewer) SetJsBundle(reader io.Reader) error {
// 	backendUrl := fmt.Sprintf("http://%v:%v", GetBackendHostname(p.instanceId), p.jsBundleLoaderPort)
// 	backendReq, err := http.NewRequest("POST", backendUrl, reader)
// 	if err != nil {
// 		return err
// 	}

// 	rsp, err := http.DefaultClient.Do(backendReq)
// 	if err != nil {
// 		return err
// 	}

// 	if rsp.StatusCode != http.StatusOK {
// 		bodybuf, _ := ioutil.ReadAll(rsp.Body)
// 		return fmt.Errorf("response code is not ok, code = %v, body = %v", rsp.StatusCode, string(bodybuf))
// 	}
// 	return nil
// }

func (p *HarmonicaPreviewer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
		p.WebSocketProxy(w, r)
	} else {
		p.HttpProxy(w, r)
	}
}

func (p *HarmonicaPreviewer) WebSocketProxy(rw http.ResponseWriter, req *http.Request) {
	backendUrl := fmt.Sprintf("ws://%v", p.getBackendHostname())
	dialer := websocket.DefaultDialer
	requestHeader := http.Header{}
	if origin := req.Header.Get("Origin"); origin != "" {
		requestHeader.Add("Origin", origin)
	}
	for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] {
		requestHeader.Add("Sec-WebSocket-Protocol", prot)
	}
	for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] {
		requestHeader.Add("Cookie", cookie)
	}
	if req.Host != "" {
		requestHeader.Set("Host", req.Host)
	}
	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		if prior, ok := req.Header["X-Forwarded-For"]; ok {
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
		}
		requestHeader.Set("X-Forwarded-For", clientIP)
	}

	requestHeader.Set("X-Forwarded-Proto", "http")
	if req.TLS != nil {
		requestHeader.Set("X-Forwarded-Proto", "https")
	}
	log.Printf("[previewer-proxy] dial backend %v", backendUrl)
	connBackend, resp, err := dialer.Dial(backendUrl, requestHeader)
	if err != nil {
		log.Printf("websocketproxy: dial backend failed, %v", err)
		if resp != nil {
			if err := copyResponse(rw, resp); err != nil {
				log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err)
			}
		} else {
			http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
		}
		return
	}
	defer connBackend.Close()

	upgrader := &websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
	}
	upgradeHeader := http.Header{}
	if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" {
		upgradeHeader.Set("Sec-Websocket-Protocol", hdr)
	}
	if hdr := resp.Header.Get("Set-Cookie"); hdr != "" {
		upgradeHeader.Set("Set-Cookie", hdr)
	}

	connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
	if err != nil {
		log.Printf("websocketproxy: couldn't upgrade %s", err)
		return
	}
	defer connPub.Close()

	errClient := make(chan error, 1)
	errBackend := make(chan error, 1)

	lastt := time.Now().Unix()
	replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) {
		for {
			msgType, msg, err := src.ReadMessage()
			if err != nil {
				m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
				if e, ok := err.(*websocket.CloseError); ok {
					if e.Code != websocket.CloseNoStatusReceived {
						m = websocket.FormatCloseMessage(e.Code, e.Text)
					}
				}
				errc <- err
				dst.WriteMessage(websocket.CloseMessage, m)
				break
			}
			err = dst.WriteMessage(msgType, msg)
			if err != nil {
				errc <- err
				break
			}

			lastt = time.Now().Unix()
		}
	}

	go replicateWebsocketConn(connPub, connBackend, errClient)
	go replicateWebsocketConn(connBackend, connPub, errBackend)

	timeoutC := make(chan bool)
	// timer
	go func(timeout int) {
		ticker := time.NewTicker(1 * time.Second)
		for range ticker.C {
			now := time.Now().Unix()
			if now-lastt > int64(timeout) {
				timeoutC <- true
			}
		}
	}(p.wsTimeout)

	//TODO: add session check job

	var message string
	select {
	case err = <-errClient:
		message = "websocketproxy: Error when copying from backend to client: %v"
	case err = <-errBackend:
		message = "websocketproxy: Error when copying from client to backend: %v"
	case <-timeoutC:
		message = "websocketproxy: timeout"
	}
	if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
		log.Printf(message, err)
	}
}

func (p *HarmonicaPreviewer) HttpProxy(rw http.ResponseWriter, req *http.Request) {
	backendUrl, err := url.Parse(fmt.Sprintf("http://%v", p.getBackendHostname()))
	if err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		rw.Write([]byte(err.Error()))
		return
	}
	backendUrl.Path = req.URL.Path
	backendUrl.Fragment = req.URL.Fragment
	backendUrl.RawQuery = req.URL.RawQuery

	backendReq, err := http.NewRequest(req.Method, backendUrl.String(), req.Body)
	if err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		rw.Write([]byte(err.Error()))
		return
	}
	defer req.Body.Close()

	rsp, err := http.DefaultClient.Do(backendReq)
	if err != nil {
		rw.WriteHeader(http.StatusInternalServerError)
		rw.Write([]byte(err.Error()))
		return
	}

	for k, vs := range rsp.Header {
		for _, v := range vs {
			rw.Header().Add(k, v)
		}
	}
	rw.Header().Set("Content-Type", "")

	rspBuf, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		rw.WriteHeader(http.StatusInternalServerError)
		rw.Write([]byte("read body err"))
		log.Printf("read body err, %v", err)
		return
	}

	if _, err = rw.Write(rspBuf); err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		rw.Write([]byte(err.Error()))
		return
	}
	defer rsp.Body.Close()
}

func copyHeader(dst, src http.Header) {
	for k, vv := range src {
		for _, v := range vv {
			dst.Add(k, v)
		}
	}
}

func copyResponse(rw http.ResponseWriter, resp *http.Response) error {
	copyHeader(rw.Header(), resp.Header)
	rw.WriteHeader(resp.StatusCode)
	defer resp.Body.Close()

	_, err := io.Copy(rw, resp.Body)
	return err
}
